<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\annotations\parsers;

#use \ReflectionException;

class ArrayParserImpl extends ArrayParser
{
	const DEFAULT_KEY_RESTRICTIONS =
		'#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#';
	
	/**
	 * Strict mode always on.
	 */
	const STRICT_MODE_ON = 1;
	
	/**
	 * Strict mode always off.
	 */
	const STRICT_MODE_OFF = 2;
	
	/**
	 * Strict mode off only at the first level.
	 */
	const STRICT_MODE_LEVEL1 = 3;
	
	/**
	 * @var ConstantValueParser
	 */
	private $constantParser;
	
	/**
	 * @var AnnotationParser
	 */
	private $annotationParser;
	
	/**
	 * @var string?
	 */
	private $keyPattern;
	
	/**
	 * @var bool
	 */
	private $strict = false;
	
	/**
	 * @var bool
	 */
	private $itemSkipping = false;
	
	/**
	 * @param ConstantValueParser $constantParser
	 * @param AnnotationParser $annotationParser
	 */
	public function __construct(
		ConstantValueParser $constantParser = null,
		AnnotationParser $annotationParser = null)
	{
		$this
			->withoutKeyRestrictions()
			->skipAnnotationValues()
			->allowMixedArrays()
			->disallowItemSkipping();
		
		if ($constantParser !== null)
		{
			$this->withConstantValueParser($constantParser);
		}
		
		if ($annotationParser !== null)
		{
			$this->withAnnotationParser($annotationParser);
		}
	}
	
	/**
	 * @param  ConstantValueParser $constantParser
	 * @return ArrayParserImpl
	 */
	public function withConstantValueParser(ConstantValueParser $constantParser)
	{
		$this->constantParser = $constantParser;
		
		return $this;
	}
	
	/**
	 * @param  AnnotationParser $annotationParser
	 * @return ArrayParserImpl
	 */
	public function withAnnotationParser(AnnotationParser $annotationParser)
	{
		$this->annotationParser = $annotationParser;
		
		return $this;
	}
	
	/**
	 * @return ArrayParserImpl
	 */
	public function skipAnnotationValues()
	{
		$this->annotationParser = null;
		
		return $this;
	}
	
	/**
	 * @param  string $keyPattern
	 * @return ArrayParserImpl
	 */
	public function withKeyRestrictions(
		$keyPattern = self::DEFAULT_KEY_RESTRICTIONS)
	{
		$this->keyPattern = $keyPattern;
		
		return $this;
	}
	
	/**
	 * @return ArrayParserImpl
	 */
	public function withoutKeyRestrictions()
	{
		$this->keyPattern = null;
		
		return $this;
	}
	
	/**
	 * @return ArrayParserImpl
	 */
	public function allowMixedArrays()
	{
		$this->strict = self::STRICT_MODE_OFF;
		
		return $this;
	}
	
	/**
	 * @return ArrayParserImpl
	 */
	public function allowMixedArraysAtFirstLevel()
	{
		 $this->strict = self::STRICT_MODE_LEVEL1;
		 
		 return $this;
	}
	
	/**
	 * @return ArrayParserImpl
	 */
	public function disallowMixedArrays()
	{
		$this->strict = self::STRICT_MODE_ON;
		
		return $this;
	}
	
	/**
	 * @return ArrayParserImpl
	 */
	public function allowItemSkipping()
	{
		$this->itemSkipping = true;
		
		return $this;
	}
	
	/**
	 * @return ArrayParserImpl
	 */
	public function disallowItemSkipping()
	{
		$this->itemSkipping = false;
		
		return $this;
	}
	
	/**
	 * @param  string $array
	 * @return array
	 * @throws ReflectionException
	 */
	public function parseArray($array)
	{
		$input = trim($array);
		
		unset($array);
		
		if (preg_match('#^(\{\s*\}|\[\s*\])$#s', $input))
		{
			return array();
		}
		
		if (empty($input) || ($input[0] !== '{' && $input[0] !== '['))
		{
			throw new ParserException(sprintf(
				'Invalid start of the array. Expected <{> or <[>, got [%s].',
				isset($input[0]) ? $input[0] : 'NULL'
			));
		}
		
		if ($this->constantParser === null)
		{
			throw new ParserException(
				'Invalid state. No ConstantValueParser specified.');
		}
		
		$itemSkipping = $this->itemSkipping;
		$strict       = $this->strict;
		$keyPattern   = $this->keyPattern;
		
		$token     = null;
		$key       = null;
		$keys      = new SplStack();
		$value     = array();
		$values    = new SplStack();
		$escape    = false;
		$enclosed  = null;
		$depth     = new SplStack();
		$skipItem  = false;
		$inAnnotation = 0;
		
		$length = strlen($input);
		
		for ($i = 0, $pos = 1; $i < $length; ++$i, ++$pos)
		{
			$t = $input[$i];
			$n = isset($input[$i + 1]) ? $input[$i + 1] : null;
			
			switch ($t)
			{
				// Start of an array
				case '{':
				case '[':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($enclosed !== null)
					{
						$token .= $t;
						
						break;
					}
					
					$skipItem = false;
					
					$depth->push($t);
					
					if ($depth->count() !== 1)
					{
						$keys->push($key);
						$values->push($value);
					}
					
					$key   = null;
					$value = array();
					break;
				
				// End of an array
				case '}':
				case ']':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($enclosed === '"' || $enclosed === "'")
					{
						$token .= $t;
						
						break;
					}
					
					// Is annotation the last element?
					if ($enclosed === '@')
					{
						if ($n !== null)
						{
							$token .= $t;
							
							break;
						}
						
						$token = '@' . $token;
					}
					
					$skipItem = $token === null;
					
					if (!$skipItem && $token !== null)
					{
						$skipItem = true;
						
						$this->setArrayItem(
						// @todo get rid of that
							$value,
							$token,
							$key,
							$depth->top(),
							$pos,
							$depth->count(),
							$itemSkipping,
							$strict,
							$keyPattern);
					}
					
					$bracket = $depth->pop();
					
					if ($bracket === '{' && $t === ']')
					{
						throw new ParserException(sprintf(
							'Bracket mismatch. Expected <}>, got <]> at position [%d].', $pos
						));
					}
					
					if ($bracket === '[' && $t === '}')
					{
						throw new ParserException(sprintf(
							'Bracket mismatch. Expected <]>, got <}> at position [%d].', $pos
						));
					}
					
					if ($depth->isEmpty())
					{
						if (($i + 1) !== $length)
						{
							throw new ParserException(sprintf(
								'Neither <}> nor <]> is the last character. ' .
								'Unexpected [%s] at position [%d].', $n, $pos
							));
						}
						
						return $value;
					}
					
					$k = $keys->pop();
					$v = $values->pop();
					
					if ($k === null)
					{
						$v[] = $value;
					}
					else
					{
						$v[$k] = $value;
					}
					
					$value = $v;
					break;
				
				// End of an element
				case ',':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($skipItem === true)
					{
						$skipItem = false;
						
						break;
					}
					
					if ($enclosed === '@' && $inAnnotation === 0)
					{
						$token    = '@' . $token;
						$enclosed = null;
					}
					
					if ($enclosed !== null)
					{
						$token .= ',';
						
						break;
					}
					
					$skipItem = false;
					
					$this->setArrayItem(
						// @todo get rid of that
						$value,
						$token,
						$key,
						$depth->top(),
						$pos,
						$depth->count(),
						$itemSkipping,
						$strict,
						$keyPattern);
					break;
				
				// End of a key
				case '=':
				case ':':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($inAnnotation === 0 && $enclosed === '@')
					{
						throw new ParserException(sprintf(
							'Can not use annotation as an item key. ' .
							'Unexpected [%s] at position [%d].', $t, $pos
						));
					}
					
					if ($enclosed !== null)
					{
						$token .= $t;
						
						break;
					}
					
					if ($key !== null)
					{
						throw new ParserException(sprintf(
							'Invalid use of key-defining character ' .
							'(two names for one element?). ' .
							'Unexpected [%s] at position [%d].', $t, $pos
						));
					}
					
					if ($token === null)
					{
						$token = null;
						
						throw new ParserException(sprintf(
							'Invalid use of key-defining character ' .
							'(forgot to give a name for the key?). ' .
							'Unexpected [%s] at position [%d].', $t, $pos
						));
					}
					
					$key   = $token;
					$token = null;
					break;
				
				// Possible start of annotation
				case '@':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($enclosed !== null)
					{
						$token .= $t;
						
						break;
					}
					
					$enclosed = '@';
					break;
				
				// Possible start of annotation's parameters
				case '(':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($enclosed !== '"' && $enclosed !== "'")
					{
						++$inAnnotation;
					}
					
					$token .= $t;
					break;
				
				// Possible end of annotation's parameters
				case ')':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($enclosed === '@')
					{
						--$inAnnotation;
						
						if ($inAnnotation === 0)
						{
							$token    = '@' . $token;
							$enclosed = null;
						}
					}
					
					$token .= $t;
					break;
				
				// Escape character
				case '\\':
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($enclosed !== "'" && $enclosed !== '"')
					{
						throw new ParserException(sprintf(
							'Invalid placement of escape character. ' .
							'Unexpected <\\> at position [%d].', $pos
						));
					}
					
					$token .= $t;
					$escape = !$escape;
					break;
				
				// Start/end of a string
				case '"':
				case "'":
					if ($enclosed === '*')
					{
						break;
					}
					
					if ($escape === true)
					{
						$token .= $t;
						$escape = false;
						
						break;
					}
					
					if ($enclosed === null || $enclosed === '@')
					{
						$enclosed = $t;
						$token .= '"';
						
						break;
					}
					
					if (($enclosed === '"' && $t === '"') ||
						($enclosed === "'" && $t === "'"))
					{
						$enclosed = $inAnnotation === 0 ? null : '@';
						$token   .= '"';
						
						break;
					}
					
					$token .= $t === '"' ? '\\"' : "'";
					break;
				
				case '/';
					if ($enclosed === null && $n === '*')
					{
						++$i;
						$enclosed = '*';
						
						break;
					}
					
					$token .= $t;
					break;
				
				case '*':
					if ($enclosed === '*' && $n === '/')
					{
						++$i;
						$enclosed = null;
						
						break;
					}
					
					$token .= $t;
					break;
				
				default:
					if ($enclosed !== '*')
					{
						if (($enclosed !== '"') &&
							($enclosed !== "'") && ($enclosed !== '@') && ctype_space($t))
						{
							break;
						}
						
						$escape = false;
						$token .= $t;
					}
					break;
			}
		}
		
		throw new ParserException(sprintf(
			'Invalid array nesting level. Expected <0>, ended with [%d]. Input: %s',
			$depth->count(), PHP_EOL . $input
		));
	}
	
	/**
	 * @param  array $array
	 * @param  mixed $value
	 * @param  mixed $key
	 * @param  char $bracketType
	 * @param  int $pos
	 * @param  int $depth
	 * @param  bool $itemSkipping
	 * @param  ArrayParserImpl::STRICT_MODE_* $strict
	 * @param  string? $keyPattern
	 * @throws ReflectionException
	 */
	private function setArrayItem(
		// @todo get rid of that
		array &$array,
		&$value,
		&$key,
		$bracketType,
		$pos,
		$depth = 1,
		$itemSkipping = false,
		$strict = self::STRICT_MODE_OFF,
		$keyPattern = null)
	{
		if ($value === null)
		{
			if ($itemSkipping === false)
			{
				throw new ParserException(sprintf(
					'No value specified for item with key [%s] around position [%d].',
					$key === null ? 'NULL' : $key, $pos
				));
			}
			
			$array[] = null;
			$key     = null;
			
			return;
		}
		
		if (($strict === self::STRICT_MODE_ON) ||
			(($depth > 1) && ($strict === self::STRICT_MODE_LEVEL1)))
		{
			if (($key !== null) && ($bracketType === '['))
			{
				throw new ParserException(sprintf(
					'Numeric arrays can not have keys when in strict mode. ' . 
					'Unexpected key [%s] around position [%d].', $key, $pos
				));
			}
			
			if (($key === null) && ($bracketType === '{'))
			{
				throw new ParserException(sprintf(
					'Associative arrays must have keys defined when in strict mode. ' .
					'No key specified around position [%d].', $pos
				));
			}
		}
		
		if (($key !== null) &&
			($keyPattern !== null) && (preg_match($keyPattern, $key) === 0))
		{
			throw new ParserException(sprintf(
				'[%s] does not conform to the specified key restrictions: [%s].',
				$key, $keyPattern
			));
		}
		
		if ($value[0] === '@')
		{
			$value = $this->annotationParser === null
				? null : $this->annotationParser->parseAnnotation($value);
		}
		else
		{
			$value = $this->constantParser->parseConstantValue($value);
		}
		
		if ($key === null)
		{
			$array[] = $value;
		}
		else
		{
			$array[$key] = $value;
		}
		
		$key   = null;
		$value = null;
	}
}